using System;
using System.Collections.Generic;
using System.Text;

using csammisrun.utility;
using csammisrun.MsnLib.Utility;

namespace csammisrun.MsnLib
{
  /// <summary>
  /// Describes the type of a message
  /// </summary>
  public enum MessageType
  {
    /// <summary>
    /// Unknown message type
    /// </summary>
    Unknown,
    /// <summary>
    /// This message is a plain text message
    /// </summary>
    Text,
    /// <summary>
    /// This message indicates that the sender has started typing
    /// </summary>
    Typing,
    /// <summary>
    /// This message summarizes the capabilities of the sender's client
    /// </summary>
    Capabilities,
    /// <summary>
    /// This message is the single dumbest thing that any IM protocol implements, period.
    /// </summary>
    Nudge,
    /// <summary>
    /// And I don't even know what this is.  I think it's for file transfer messages
    /// </summary>
    SLP
  }

  /// <summary>
  /// Describes the types of errors that a message can generate
  /// </summary>
  public enum MessageErrorType
  {
    None,
    Timeout,
    NoAck,
    SwitchboardError,
    Unknown
  }

  class SlpHeader
  {
    public const int SIZEOF = 48;

    public SlpHeader(byte[] data, ref int index)
    {
      SessionID = Marshal.ByteArrayToUint32(data, ref index);
      MsgID =     Marshal.ByteArrayToUint32(data, ref index);
      Offset =    Marshal.ByteArrayToUint64(data, ref index);
      TotalSize = Marshal.ByteArrayToUint64(data, ref index);
      Length =    Marshal.ByteArrayToUint32(data, ref index);
      Flags =     Marshal.ByteArrayToUint32(data, ref index);
      AckID =     Marshal.ByteArrayToUint32(data, ref index);
      AckSubID =  Marshal.ByteArrayToUint32(data, ref index);
      AckSize =   Marshal.ByteArrayToUint64(data, ref index);
    }

    public SlpHeader()
    {
    }

    public byte[] ToByteArray()
    {
      byte[] retval = new byte[SlpHeader.SIZEOF];
      int i = 0;
      int index = 0;
      ulong mask = 0xFF;
      for (i = 0; i < 4; i++, index++, mask <<= 8)
      {
        retval[index] = (byte)((SessionID & mask) >> (i * 8));
      }
      for (i = 0, mask = 0xFF; i < 4; i++, index++, mask <<= 8)
      {
        retval[index] = (byte)((MsgID & mask) >> (i * 8));
      }
      for (i = 0, mask = 0xFF; i < 8; i++, index++, mask <<= 8)
      {
        retval[index] = (byte)((Offset & mask) >> (i * 8));
      }
      for (i = 0, mask = 0xFF; i < 8; i++, index++, mask <<= 8)
      {
        retval[index] = (byte)((TotalSize & mask) >> (i * 8));
      }
      for (i = 0, mask = 0xFF; i < 4; i++, index++, mask <<= 8)
      {
        retval[index] = (byte)((Length & mask) >> (i * 8));
      }
      for (i = 0, mask = 0xFF; i < 4; i++, index++, mask <<= 8)
      {
        retval[index] = (byte)((Flags & mask) >> (i * 8));
      }
      for (i = 0, mask = 0xFF; i < 4; i++, index++, mask <<= 8)
      {
        retval[index] = (byte)((AckID & mask) >> (i * 8));
      }
      for (i = 0, mask = 0xFF; i < 4; i++, index++, mask <<= 8)
      {
        retval[index] = (byte)((AckSubID & mask) >> (i * 8));
      }
      for (i = 0, mask = 0xFF; i < 8; i++, index++, mask <<= 8)
      {
        retval[index] = (byte)((AckSize & mask) >> (i * 8));
      }
      return retval;
    }

    public UInt32 SessionID = 0;
    public UInt32 MsgID = 0;
    public UInt64 Offset = 0;
    public UInt64 TotalSize = 0;
    public UInt32 Length = 0;
    public UInt32 Flags = 0;
    public UInt32 AckID = 0;
    public UInt32 AckSubID = 0;
    public UInt64 AckSize = 0;
  }

  class SlpFooter
  {
    public UInt32 Value;
  }

  public class Message
  {
    #region Static message conversion methods
    /// <summary>
    /// Convert a well-formed HTML string to a plain text message
    /// </summary>
    public static Message HTMLToTextMessage(string html)
    {
      Message msg = new Message(MessageType.Text);
      
      if (html.ToLower().StartsWith("<html>"))
      {
        html = html.Substring(6);
        html = html.Substring(0, html.Length - 7);
      }

      int tagendindex = 0;
      int tagnamelength = 0;
      int closetaglength = 0;
      string tagcontents = "";

      // Pull off starting HTML tags
      List<string> formatting = new List<string>();
      while (html[0] == '<')
      {
        tagendindex = html.IndexOf('>');
        tagcontents = html.Substring(1, tagendindex - 1);
        tagnamelength = html.IndexOfAny(new char[] { ' ', '>' }) - 1;
        closetaglength = tagnamelength + 3; // < / tagname >
        html = html.Substring(tagendindex + 1, html.Length - tagendindex - closetaglength - 1);
        formatting.Add(tagcontents);
      }

      // Clip out all HTML tags in the middle of the message, MSN doesn't support inline HTML
      string bodytext = System.Text.RegularExpressions.Regex.Replace(html, "<.*?>", "");

      // Build the X-MMS-IM-Format attribute
      string format = "";
      string texteffects = "";
      foreach (string frmt in formatting)
      {
        string tag = frmt.ToLower();
        if (tag == "b")
        {
          texteffects += "B";
        }
        else if (tag == "i")
        {
          texteffects += "I";
        }
        else if (tag == "u")
        {
          texteffects += "U";
        }
        else if (tag == "s")
        {
          texteffects += "S";
        }
        else if (tag.StartsWith("font"))
        {
          string[] attributes = tag.Split(' ');
          foreach (string attribute in attributes)
          {
            string[] kvp = attribute.Split('=');
            if (kvp.Length != 2)
            {
              continue;
            }

            kvp[1] = kvp[1].Trim('"');
            switch (kvp[0])
            {
              case "face":
                format += "FN=" + kvp[1] + ";";
                break;
              case "color":
                format += "CO=" + kvp[1].Trim('#') + ";";
                break;
            }
          }
        }
      }

      if (texteffects != "")
      {
        format += "EF=" + texteffects + ";";
      }

      // Fill out the message and return
      if(format != "")
      {
        msg.SetAttribute("X-MMS-IM-Format", format);
      }
      msg.Body = Encoding.UTF8.GetBytes(bodytext);
      // TODO:  Make sure all the rest of the attributes are filled out
      return msg;
    }

    /// <summary>
    /// Converts a plain text message to well-formed HTML
    /// </summary>
    public static string TextMessageToHTML(Message msg)
    {
      if (msg.Type != MessageType.Text)
      {
        throw new ArgumentException("Message must be of type Text");
      }

      // If the message doesn't have a body, return an empty document
      if (msg.Body == null)
      {
        return "<html></html>";
      }

      // Get the HTML-encoded body string
      string body = HttpUtility.HtmlEncode(Encoding.UTF8.GetString(msg.Body));

      if (msg.Attributes("X-MMS-IM-Format") == "")
      {
        return "<html>" + body + "</html>";
      }

      StringBuilder prefix = new StringBuilder();
      StringBuilder postfix = new StringBuilder();
      string format = "";
      int attribindex = 0, endindex = 0, tmp = 0;
      List<int> colors = new List<int>();

      format = msg.Attributes("X-MMS-IM-Format");
      // Check for the font face attribute
      attribindex = format.IndexOf("FN=");
      if (attribindex != -1 && format[attribindex = attribindex + 3] != ';')
      {
        endindex = format.IndexOf(';', attribindex);
        prefix.AppendFormat("<font face=\"{0}\">", format.Substring(attribindex, endindex - attribindex));
        postfix.Insert(0, "</font>");
      }
      // Check for font attributes
      attribindex = format.IndexOf("EF=");
      if (attribindex != -1 && format[attribindex = attribindex + 3] != ';')
      {
        endindex = format.IndexOf(';', attribindex);
        foreach (char c in format.Substring(attribindex, endindex - attribindex).ToLower())
        {
          prefix.AppendFormat("<{0}>", c);
          postfix.Insert(0, String.Format("</{0}>", c));
        }
      }
      // Check for colors
      attribindex = format.IndexOf("CO=");
      if (attribindex != -1 && format[attribindex = attribindex + 3] != ';')
      {
        endindex = format.IndexOf(';', attribindex) - 1;
        do
        {
          if (attribindex + 2 < endindex)
          {
            colors.Add(Int32.Parse(format.Substring(attribindex, 2), System.Globalization.NumberStyles.HexNumber));
          }
          else
          {
            colors.Add(0);
          }
          attribindex += 2;
        } while (attribindex < endindex);

        if (colors.Count != 0)
        {
          if (colors.Count == 1)
          {
            colors.Add(0);
            colors.Add(0);
          }
          else if (colors.Count == 2)
          {
            tmp = colors[0];
            colors[0] = colors[1];
            colors[1] = tmp;
            colors.Add(0);
          }
          else if (colors.Count == 3)
          {
            tmp = colors[0];
            colors[0] = colors[2];
            colors[2] = tmp;
          }

          prefix.AppendFormat("<font color=\"#{0:x2}{1:x2}{2:x2}\">", colors[0], colors[1], colors[2]);
          postfix.Insert(0, "</font>");
        }
      }

      return HttpUtility.UrlDecode(prefix.ToString()) + body + HttpUtility.UrlDecode(postfix.ToString());
    }
    #endregion

    private MessageType _type = MessageType.Unknown;
    private bool _isslpmessage = false;
    private string _remoteuser = "";
    private char _flag = '\0';
    private string _contenttype = "";
    private string _charset = "";
    private byte[] _body = null;
    private SlpHeader _header = null;
    private SlpFooter _footer = new SlpFooter();
    private MsnCommand _command = null;
    private Transaction _transaction = null;
    private Dictionary<string, string> _attribtable = new Dictionary<string, string>();

    #region Constructors
    /// <summary>
    /// Creates a new message of the specified type
    /// </summary>
    public Message(MessageType type)
    {
      _type = type;
      switch (_type)
      {
        case MessageType.SLP:
          _attribtable["User-Agent"] = null;
          _isslpmessage = true;
          _flag = 'D';
          _attribtable["MIME-Version"] = "1.0";
          _contenttype = "application/x-msnmsgrp2p";
          _header = new SlpHeader();
          _footer = new SlpFooter();
          break;
        case MessageType.Nudge:
          _attribtable["ID"] = "1\r\n";
          _flag = 'N';
          _contenttype = "text/x-msnmsgr-datacast\r\n";
          break;
        case MessageType.Text:
          _flag = 'A';
          _charset = "UTF-8";
          _attribtable["User-Agent"] = "MsnLib/" + Session.MSNLIB_VERSION;
          _contenttype = "text/plain";
          break;
        case MessageType.Typing:
          _flag = 'U';
          _contenttype = "text/x-msmsgscontrol";
          SetBinaryData(new byte[] {0x13, 0x10}); // \r\n
          break;
      }
    }

    /// <summary>
    /// Creates a new plaintext message
    /// </summary>
    public Message(string message)
    {
      _type = MessageType.Text;
      _attribtable["User-Agent"] = "MsnLib/" + Session.MSNLIB_VERSION;
      _attribtable["X-MMS-IM-Format"] = "FN=MS%20Sans%20Serif; EF=; CO=0; PF=0";
      _contenttype = "text/plain";
      _charset = "UTF-8";
      _flag = 'A';
      _body = Encoding.UTF8.GetBytes(Marshal.EnsureCRLF(message));

      // Truncate the body to the appropriate length
      if (_body.Length > 1664)
      {
        byte[] newbody = new byte[1664];
        Array.Copy(_body, newbody, 1664);
        _body = newbody;
      }
    }

    /// <summary>
    /// Creates a new message from a received command
    /// </summary>
    public Message(Session sess, MsnCommand command)
    {
      _type = MessageType.Unknown;
      _remoteuser = command.Parameters[0];
      _command = command;
    }
    #endregion

    #region Methods
    public void ParseSLPHeader(byte[] body)
    {
      int index = 0;
      _header = new SlpHeader(body, ref index);
      if (index < body.Length)
      {
        _body = new byte[body.Length - index];
        Array.Copy(body, index, _body, 0, _body.Length);
      }
    }

    /// <summary>
    /// Generates a payload for this message
    /// </summary>
    public byte[] GeneratePayload()
    {
      byte[] payload = null;
      string header = "";
      string attribute = "";
      int headerlen = 0;
      int attributelen = 0;
      int index = 0;

      payload = new byte[8192];

      // Insert the standard header
      if (String.IsNullOrEmpty(_charset))
      {
        header = String.Format("MIME-Version: 1.0\r\nContent-Type: {0}\r\n", _contenttype);
      }
      else
      {
        header = String.Format("MIME-Version: 1.0\r\nContent-Type: {0}; charset={1}\r\n", _contenttype, _charset);
      }
      headerlen = Encoding.UTF8.GetByteCount(header);
      Array.Copy(Encoding.UTF8.GetBytes(header), 0, payload, index, headerlen);
      index += headerlen;

      // Insert any attributes this message has
      foreach (KeyValuePair<string, string> kvp in _attribtable)
      {
        if (kvp.Value == null)
        {
          continue;
        }

        attribute = String.Format("{0}: {1}\r\n", kvp.Key, kvp.Value);
        attributelen = Encoding.UTF8.GetByteCount(attribute);
        Array.Copy(Encoding.UTF8.GetBytes(attribute), 0, payload, index, attributelen);
        index += attributelen;
      }

      Array.Copy(new byte[] { (byte)'\r', (byte)'\n' }, 0, payload, index, 2);
      index += 2;

      if (this.IsSLPMessage)
      {
        // Insert the SLP header
        Array.Copy(_header.ToByteArray(), 0, payload, index, SlpHeader.SIZEOF);
        index += 48;
      }
      if (_body != null)
      {
        Array.Copy(_body, 0, payload, index, _body.Length);
        index += _body.Length;
      }
      if (this.IsSLPMessage)
      {
        for (uint i = 0, mask = 0xFF; i < 4; i++, index++)
        {
          payload[index] = (byte)((_footer.Value & mask) >> ((int)i * 8));
        }
      }

      // Bigger than 1664?  No sir!
      byte[] retval = new byte[((index > 1664) ? 1664 : index)];
      Array.Copy(payload, retval, retval.Length);
      Logging.WriteString("Sending payload: \r\n" + Encoding.UTF8.GetString(retval));
      return retval;
    }

    public void ParsePayload(byte[] payload)
    {
      string ascii = Encoding.UTF8.GetString(payload);

      Logging.WriteString("Received message:\r\n" + ascii);
      string[] elements = null;
      string[] tokens = null;
      string[] elementsplitter = new string[] { ": " };
      int idxsemi = -1;
      int idxequals = -1;
      int end = -1;
      int index = 0;

      end = ascii.IndexOf("\r\n\r\n");
      if (end == -1)
      {
        // Attempt a fallback where Gaim just craps out
        // This may seriously screw with messages in some charsets, I don't know
        ascii = Marshal.EnsureCRLF(ascii);
        end = ascii.IndexOf("\r\n\r\n");
        if (end == -1)
        {
          return;
        }
      }

      elements = ascii.Substring(0, end).Split(new string[] { "\r\n" }, StringSplitOptions.None);
      foreach (string element in elements)
      {
        tokens = element.Split(elementsplitter, StringSplitOptions.None);

        switch (tokens[0])
        {
          case "MIME-Version":
            break;

          case "Content-Type":
            // See if the charset is defined in there, grab it if it is
            if ((idxsemi = tokens[1].IndexOf(';')) != -1)
            {
              if ((idxequals = tokens[1].IndexOf('=', idxsemi)) != -1)
              {
                idxequals++;
                _charset = tokens[1].Substring(idxequals);
              }
              _contenttype = tokens[1].Substring(0, idxsemi);
            }
            else
            {
              _contenttype = tokens[1];
            }
            break;

          default:
            _attribtable[tokens[0]] = tokens[1];
            break;
        }
      }

      index = end + 4; // Skip the double CRLF

      // Bring what's left of the body back to a byte array

      byte[] tmpbody = new byte[payload.Length - index];
      Array.ConstrainedCopy(payload, index, tmpbody, 0, tmpbody.Length);

      if (_contenttype == "application/x-msnmsgrp2p")
      {
        if (tmpbody.Length < SlpHeader.SIZEOF)
        {
          // oops
          return;
        }

        _isslpmessage = true;

        // This will extract the part of the parameter that is left after the header into _body,
        ParseSLPHeader(tmpbody);
        // but we still need to get the last four bytes off it into the SLP footer
        index = _body.Length - 4;
        tmpbody = new byte[index];
        Array.Copy(_body, tmpbody, tmpbody.Length);
        _body = tmpbody;
      }
      else
      {
        _body = tmpbody;
      }
    } // End ParsePayload

    /// <summary>
    /// Directly set a message payload 
    /// </summary>
    /// <remarks>This method will truncate <paramref name="data"/> to 1664 bytes to accomodate MSN's maximum message size</remarks>
    public void SetBinaryData(byte[] data)
    {
      int length = (data.Length > 1664) ? 1664 : data.Length;
      _body = new byte[length];
      Array.Copy(data, _body, length);
    }

    /// <summary>
    /// Gets the newline-seperated fields from the body of a message
    /// </summary>
    public Dictionary<string, string> GetFieldsFromBody()
    {
      Dictionary<string, string> retval = new Dictionary<string, string>();
      string[] fieldsplitter = new string[] { ": " };
      string[] tokens = null;
      string[] fields = null;

      if (_body == null)
      {
        return retval;
      }

      fields = Encoding.UTF8.GetString(_body).Split(
        new string[] { "\r\n" }, StringSplitOptions.None);

      foreach (string field in fields)
      {
        tokens = field.Split(fieldsplitter, StringSplitOptions.None);
        retval[tokens[0].Trim()] = tokens[1].Trim();
      }

      return retval;
    }
    #endregion

    #region Properties
    /// <summary>
    /// Gets or sets the <see cref="MessageType"/> of this message
    /// </summary>
    public MessageType Type
    {
      get { return _type; }
      set { _type = value; }
    }

    /// <summary>
    /// Gets or sets a value indicating whether or not this message is a SLP message
    /// </summary>
    public bool IsSLPMessage
    {
      get { return _isslpmessage; }
      set { _isslpmessage = value; }
    }

    /// <summary>
    /// Gets or sets the Passport of the remote user
    /// </summary>
    public string RemoteUser
    {
      get { return _remoteuser; }
      set { _remoteuser = value; }
    }

    /// <summary>
    /// Gets or sets a flag that I don't know what it does
    /// </summary>
    public char Flag
    {
      get { return _flag; }
      set { _flag = value; }
    }

    /// <summary>
    /// Gets or sets the Content Type for this message
    /// </summary>
    public string ContentType
    {
      get { return _contenttype; }
      set { _contenttype = value; }
    }

    /// <summary>
    /// Gets or sets the character set for the body of the message
    /// </summary>
    public string Charset
    {
      get { return _charset; }
      set { _charset = value; }
    }

    /// <summary>
    /// Gets or sets the body of the message
    /// </summary>
    public byte[] Body
    {
      get { return _body; }
      set { _body = value; }
    }

    /// <summary>
    /// Gets or sets the SLP Header
    /// </summary>
    internal SlpHeader Header
    {
      get { return _header; }
      set { _header = value; }
    }

    /// <summary>
    /// Gets or sets the SLP Footer
    /// </summary>
    internal SlpFooter Footer
    {
      get { return _footer; }
      set { _footer = value; }
    }

    /// <summary>
    /// Gets or sets the <see cref="MsnCommand"/> that created this message
    /// </summary>
    /// <remarks>This field will be <c>null</c> if the message was created locally</remarks>
    public MsnCommand Command
    {
      get { return _command; }
      set { _command = value; }
    }

    /// <summary>
    /// Gets or sets the <see cref="Transaction"/> that is carrying this message
    /// </summary>
    public Transaction Transaction
    {
      get { return _transaction; }
      set { _transaction = value; }
    }

    /// <summary>
    /// Gets an attribute from this message
    /// </summary>
    public string Attributes(string key)
    {
      if (_attribtable.ContainsKey(key))
      {
        return _attribtable[key];
      }
      return "";
    }

    /// <summary>
    /// Sets an attribute on this message
    /// </summary>
    public void SetAttribute(string key, string attribute)
    {
      _attribtable[key] = attribute;
    }
    #endregion
  }
}
